﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Tabs;
using System.IO;
using DotNetNuke.UI.Skins;
using System.Text.RegularExpressions;
using DotNetNuke.Entities.Modules.Definitions;
using System.Web;

namespace HeroldIT.Dnn.Modules.WebControls
{
    /// <summary>
    /// A hierarchical DropDownList for selecting a skin.
    /// </summary>
    public class SkinSelector : Panel, INamingContainer
    {
        #region constants

        private const string DefaultSkinValue = "<default>";

        #endregion

        #region static fields

        private static readonly Regex folderIndenter = new Regex("[^/]+/", RegexOptions.Compiled);

        private static readonly Regex[] folderExceptions = new[]
                                                               {
                                                                   new Regex("\\\\\\.svn\\b",
                                                                             RegexOptions.Compiled |
                                                                             RegexOptions.IgnoreCase),
                                                                   new Regex("\\\\App_LocalResources\\b",
                                                                             RegexOptions.Compiled |
                                                                             RegexOptions.IgnoreCase)
                                                               };

        private static readonly Regex[] portalFolderExceptions = new[]
                                                                     {
                                                                         new Regex("\\\\Cache\\b",
                                                                                   RegexOptions.Compiled |
                                                                                   RegexOptions.IgnoreCase),
                                                                         new Regex("\\\\Templates\\b",
                                                                                   RegexOptions.Compiled |
                                                                                   RegexOptions.IgnoreCase),
                                                                         new Regex("\\\\Containers\\b",
                                                                                   RegexOptions.Compiled |
                                                                                   RegexOptions.IgnoreCase),
                                                                         new Regex("\\\\Skins\\b",
                                                                                   RegexOptions.Compiled |
                                                                                   RegexOptions.IgnoreCase)
                                                                     };

        #endregion

        #region fields

        private HierarchicalDropDownList hddSelector;

        private UpdatePanel upnCustomPath;

        private TextBox txtCustomPath;

        #endregion

        #region properties

        private ModuleInfo module;

        /// <summary>
        /// Gets or sets the <see cref="ModuleInfo"/> using the selector.
        /// </summary>
        public ModuleInfo Module
        {
            get
            {
                if (null == this.module)
                {
                    string moduleIdValue = this.Page.Request["ModuleId"];
                    int moduleId;
                    if (!String.IsNullOrEmpty(moduleIdValue) && int.TryParse(moduleIdValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out moduleId))
                    {
                        PortalSettings portalSettings = PortalController.GetCurrentPortalSettings();
                        this.module = new ModuleController().GetModule(moduleId, portalSettings.ActiveTab.TabID, false);
                    }
                }
                return this.module;
            }
            set
            {
                this.module = value;
            }
        }

        /// <summary>
        /// Gets or sets the selected skin path.
        /// </summary>
        public string SkinPath
        {
            get
            {
                this.EnsureChildControls();

                if (this.hddSelector.SelectedValue.Equals(DefaultSkinValue))
                {
                    return null;
                }
                if (String.IsNullOrEmpty(this.hddSelector.SelectedValue) || String.IsNullOrEmpty(this.hddSelector.SelectedSlaveValue))
                {
                    return String.IsNullOrEmpty(this.txtCustomPath.Text) ? null : this.txtCustomPath.Text;
                }
                if (this.hddSelector.SelectedSlaveValue.Equals(DefaultSkinValue))
                {
                    return null;
                }
                return this.hddSelector.SelectedValue + this.hddSelector.SelectedSlaveValue;
            }
            set
            {
                this.EnsureChildControls();

                if (null == value)
                {
                    this.hddSelector.SelectedValue = DefaultSkinValue;
                }
                else if (value.StartsWith("[M]", StringComparison.OrdinalIgnoreCase)
                    || value.StartsWith("[S]", StringComparison.OrdinalIgnoreCase)
                    || value.StartsWith("[P]", StringComparison.OrdinalIgnoreCase)
                    || value.StartsWith("[C]", StringComparison.OrdinalIgnoreCase))
                {
                    string path, file;
                    int index = value.LastIndexOf('/');
                    if (index > 0)
                    {
                        path = value.Substring(0, index + 1);
                        file = value.Substring(index + 1);
                    }
                    else
                    {
                        path = value.Substring(0, 3);
                        file = value.Substring(3);
                    }
                    this.hddSelector.SelectedValue = path;
                    this.hddSelector.SelectedSlaveValue = file;
                }
                else
                {
                    this.hddSelector.SelectedValue = String.Empty;
                    this.txtCustomPath.Text = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the width of the Web server control.
        /// </summary>
        /// <returns>A <see cref="T:System.Web.UI.WebControls.Unit" /> that represents the width of the control. The default is <see cref="F:System.Web.UI.WebControls.Unit.Empty" />.</returns>
        /// <exception cref="T:System.ArgumentException">The width of the Web server control was set to a negative value.</exception>
        public override Unit Width
        {
            get
            {
                return base.Width;
            }
            set
            {
                base.Width = value;

                if (this.ChildControlsCreated)
                {
                    this.hddSelector.Width = value;
                    this.txtCustomPath.Width = value;
                }
            }
        }

        private string validationGroup;

        /// <summary>
        /// Gets or sets the group of controls for which the control causes validation
        /// when it posts back to the server.
        /// </summary>
        public string ValidationGroup
        {
            get
            {
                return this.validationGroup;
            }
            set
            {
                this.validationGroup = value;

                if (this.ChildControlsCreated)
                {
                    this.hddSelector.ValidationGroup = value;
                    this.txtCustomPath.ValidationGroup = value;
                }
            }
        }

        #endregion

        #region localization properties

        private string customUrlText = "<Custom url>";

        /// <summary>
        /// Gets or sets the text for the "Custom url" dropdown entry.
        /// </summary>
        public string CustomUrlText
        {
            get { return this.customUrlText; }
            set { this.customUrlText = value; }
        }

        private string defaultText = "<Default>";

        /// <summary>
        /// Gets or sets the text for the "default" dropdown entry.
        /// </summary>
        public string DefaultText
        {
            get { return this.defaultText; }
            set { this.defaultText = value; }
        }

        private string skinFolderText = "Skin folder";

        /// <summary>
        /// Gets or sets the text for the "Skin folder" dropdown entry.
        /// </summary>
        public string SkinFolderText
        {
            get { return this.skinFolderText; }
            set { this.skinFolderText = value; }
        }

        private string containerFolderText = "Container folder";

        /// <summary>
        /// Gets or sets the text for the "Container folder" dropdown entry.
        /// </summary>
        public string ContainerFolderText
        {
            get { return this.containerFolderText; }
            set { this.containerFolderText = value; }
        }

        private string moduleFolderText = "Module folder";

        /// <summary>
        /// Gets or sets the text for the "Module folder" dropdown entry.
        /// </summary>
        public string ModuleFolderText
        {
            get { return this.moduleFolderText; }
            set { this.moduleFolderText = value; }
        }

        private string portalRootFolderText = "Portal root folder";

        /// <summary>
        /// Gets or sets the text for the "Portal root folder" dropdown entry.
        /// </summary>
        public string PortalRootFolderText
        {
            get { return this.portalRootFolderText; }
            set { this.portalRootFolderText = value; }
        }

        #endregion

        #region page lifecycle

        /// <summary>
        /// Raises the <see cref="E:System.Web.UI.Control.Init" /> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs" /> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            this.EnsureChildControls();

            base.OnInit(e);
        }

        /// <summary>
        /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
        /// </summary>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();

            this.DataBind();

            this.hddSelector = new HierarchicalDropDownList
                                   {
                                       ID = "hddSelector",
                                       CssClass = this.CssClass,
                                       DataTextField = "Text",
                                       DataValueField = "Value",
                                       SlaveDataTextField = "Text",
                                       SlaveDataValueField = "Value",
                                       Width = this.Width,
                                       ValidationGroup = this.ValidationGroup
                                   };
            this.hddSelector.SlaveDataRequested += this.hddSelector_SlaveDataRequested;
            this.hddSelector.SelectedValueChanged += this.hddSelector_SelectedValueChanged;

            List<ListItemInfo> paths = this.FetchSkinPaths();

            // bind folders
            this.hddSelector.DataSource = paths;
            this.Controls.Add(this.hddSelector);

            this.txtCustomPath = new TextBox
                                     {
                                         ID = "txtCustomPath",
                                         Width = this.Width,
                                         ValidationGroup = this.ValidationGroup,
                                         CssClass = "SkinSelector-CustomPath",
                                         Visible = false
                                     };
            this.upnCustomPath = new UpdatePanel
                                     {
                                         ID = "upnCustomPath",
                                         ChildrenAsTriggers = true,
                                         ContentTemplate =
                                             new CompiledTemplateBuilder(
                                             control => control.Controls.Add(this.txtCustomPath)),
                                         RenderMode = UpdatePanelRenderMode.Block,
                                         UpdateMode = UpdatePanelUpdateMode.Always
                                     };
            this.Controls.Add(this.upnCustomPath);

            this.hddSelector.DataBind();
            this.hddSelector.Items.Insert(0, new ListItem(String.IsNullOrEmpty(this.DefaultText) ? "<Default>" : this.DefaultText, DefaultSkinValue));
            this.hddSelector.Items.Insert(1, new ListItem(String.IsNullOrEmpty(this.CustomUrlText) ? "<Custom url>" : this.CustomUrlText, ""));
        }

        #endregion

        #region methods

        private List<ListItemInfo> FetchSkinPaths()
        {
            List<ListItemInfo> paths = new List<ListItemInfo>();
            PortalSettings portalSettings = PortalController.GetCurrentPortalSettings();

            // fetch skin folders
            paths.Add(new ListItemInfo(String.IsNullOrEmpty(this.SkinFolderText) ? "Skin folder" : this.SkinFolderText, "[S]"));
            string skinPath = null;
            if (null != this.Module)
            {
                TabInfo tab = new TabController().GetTab(this.Module.TabID, this.Module.PortalID, false);
                skinPath = tab.SkinPath;
            }
            if (String.IsNullOrEmpty(skinPath))
            {
                skinPath = HeroldIT.Dnn.Modules.Compatibility.PortalSettings.Instance.DefaultPortalSkin;
            }
            skinPath = SkinController.FormatSkinSrc(skinPath, portalSettings);
            string skinBasePath = this.Page.Server.MapPath(skinPath);
            skinBasePath = skinBasePath.Substring(0, skinBasePath.LastIndexOf('\\'));

            var directories = Directory.GetDirectories(skinBasePath, "*", SearchOption.AllDirectories)
                .Where(d => Array.TrueForAll(folderExceptions, r => !r.IsMatch(d)))
                .OrderBy(d => d);
            foreach (string directory in directories)
            {
                string relDir = directory.Substring(skinBasePath.Length + 1).Replace('\\', '/');
                string dirText = "..." + folderIndenter.Replace(relDir, "...");
                string dirValue = "[S]" + relDir + "/";
                paths.Add(new ListItemInfo(dirText, dirValue));
            }

            // fetch container folders
            paths.Add(new ListItemInfo(String.IsNullOrEmpty(this.ContainerFolderText) ? "Container folder" : this.ContainerFolderText, "[C]"));
            string containerPath = null;
            if (null != this.Module)
            {
                containerPath = this.Module.ContainerPath;
            }
            if (String.IsNullOrEmpty(containerPath))
            {
                containerPath = HeroldIT.Dnn.Modules.Compatibility.PortalSettings.Instance.DefaultPortalContainer;
            }
            containerPath = SkinController.FormatSkinSrc(containerPath, portalSettings);
            string containerBasePath = this.Page.Server.MapPath(containerPath);
            containerBasePath = containerBasePath.Substring(0, containerBasePath.LastIndexOf('\\'));

            directories = Directory.GetDirectories(containerBasePath, "*", SearchOption.AllDirectories)
                .Where(d => Array.TrueForAll(folderExceptions, r => !r.IsMatch(d)))
                .OrderBy(d => d);
            foreach (string directory in directories)
            {
                string relDir = directory.Substring(containerBasePath.Length + 1).Replace('\\', '/');
                string dirText = "..." + folderIndenter.Replace(relDir, "...");
                string dirValue = "[C]" + relDir + "/";
                paths.Add(new ListItemInfo(dirText, dirValue));
            }

            // fetch module folders
            if (null != this.Module)
            {
                paths.Add(new ListItemInfo(String.IsNullOrEmpty(this.ModuleFolderText) ? "Module folder" : this.ModuleFolderText, "[M]"));

                string modulePath = Path.Combine(this.Page.Server.MapPath(VirtualPathUtility.ToAbsolute("~/DesktopModules")), new DesktopModuleController().GetDesktopModule(new ModuleDefinitionController().GetModuleDefinition(this.Module.ModuleDefID).DesktopModuleID).FolderName);

                directories = Directory.GetDirectories(modulePath, "*", SearchOption.AllDirectories)
                    .Where(d => Array.TrueForAll(folderExceptions, r => !r.IsMatch(d)))
                    .OrderBy(d => d);
                foreach (string directory in directories)
                {
                    string relDir = directory.Substring(modulePath.Length + 1).Replace('\\', '/');
                    string dirText = "..." + folderIndenter.Replace(relDir, "...");
                    string dirValue = "[M]" + relDir + "/";
                    paths.Add(new ListItemInfo(dirText, dirValue));
                }
            }

            // fetch portal folders
            paths.Add(new ListItemInfo(String.IsNullOrEmpty(this.PortalRootFolderText) ? "Portal root folder" : this.PortalRootFolderText, "[P]"));

            directories = Directory.GetDirectories(portalSettings.HomeDirectoryMapPath, "*", SearchOption.AllDirectories)
                .Where(d => Array.TrueForAll(folderExceptions, r => !r.IsMatch(d))
                    && Array.TrueForAll(portalFolderExceptions, r => !r.IsMatch(d)))
                .OrderBy(d => d);
            foreach (string directory in directories)
            {
                string relDir = directory.Substring(portalSettings.HomeDirectoryMapPath.Length).Replace('\\', '/');
                string dirText = "..." + folderIndenter.Replace(relDir, "...");
                string dirValue = "[P]" + relDir + "/";
                paths.Add(new ListItemInfo(dirText, dirValue));
            }
            return paths;
        }

        #endregion

        #region event handlers

        void hddSelector_SlaveDataRequested(object sender, HierarchicalDataEventArgs e)
        {
            if (String.IsNullOrEmpty(e.MasterValue) || e.MasterValue.Equals(DefaultSkinValue))
                return;

            string skinPath = this.Page.Server.MapPath(this.Module.MakeSkinUrl(e.MasterValue));
            string[] controls = Directory.GetFiles(skinPath, "*.ascx", SearchOption.TopDirectoryOnly);

            if (controls.Length <= 0)
            {
                e.SlaveDataSource = new[] { new ListItemInfo("<no skin available>", "") };
            }
            else
            {
                e.SlaveDataSource = new[] { new ListItemInfo("<Default>", DefaultSkinValue) }
                    .Concat(controls.Select(c => new ListItemInfo(c.Substring(skinPath.Length), c.Substring(skinPath.Length))));
            }
        }

        void hddSelector_SelectedValueChanged(object sender, EventArgs e)
        {
            this.txtCustomPath.Visible = String.IsNullOrEmpty(hddSelector.SelectedValue);
        }

        #endregion

        #region helper classes

        private class ListItemInfo
        {
            // ReSharper disable UnusedAutoPropertyAccessor.Local
            // ReSharper disable MemberCanBePrivate.Local
            public string Text { get; private set; }
            public string Value { get; private set; }
            // ReSharper restore MemberCanBePrivate.Local
            // ReSharper restore UnusedAutoPropertyAccessor.Local

            public ListItemInfo(string text, string value)
            {
                this.Text = text;
                this.Value = value;
            }
        }

        #endregion
    }
}
